//////////////////////////////////////////////////
// EASIM project
// easim.net.Data
// 6/20/2007 1:14 PM
// Cong Liu
// gzcong@gmail.com

// Copyright (C) 2007
// All rights reserved.
// Refer to LICENSE of JIST (Java In Simulation Time) for terms and conditions of use.

package easim.omar;

import easim.net.DtnNodeInterface;
import easim.net.Clock;
import easim.net.Timer;
import easim.net.Field;

import jist.swans.Constants;
import jist.swans.misc.Message;
import jist.runtime.JistAPI.DoNotRewrite;

import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Iterator;

public class Data implements DtnNodeInterface, DoNotRewrite
{
  public static boolean DTDV_ENABLED = true;
  public static boolean SW_ENABLED = true;
  public static double DTDV_ALLOCATION = -1;
  public static boolean DTDV_PRIORITIZED = false;

  public static long DATA_INTERVAL = 5 * Constants.SECOND;
  public static int DATA_SIZE = 1 * 1024;
  public static int BUFFER_SIZE = 100;
  public static long DATA_TTL = 100 * Constants.SECOND;
  public static int DATA_TICKETS = 100;
  public static final long INIT_TIME = - 1000 * Constants.SECOND;
  public static final long CLEANER_INTERVAL = 10 * Constants.SECOND;

    
  /** class TableItem */
  public static class DataItem implements DoNotRewrite
  {
    public int srcId = -1;
    public int dstId = -1;
    public int msgId = -1;
    public long addTime = INIT_TIME;
    public long timeout = INIT_TIME;
    public int tickets = 0;
    public int hops = 0;

    public String toString() {
      return "(" + srcId + "->" + dstId + ")" + msgId + 
        "(" + (timeout / Constants.SECOND) + ")" + tickets;
    }
    public boolean equals(Object obj) {
      if (! (obj instanceof DataItem)) return false;
      DataItem data = (DataItem)obj;
      return (srcId == data.srcId && msgId == data.msgId);
    }
    public int hashCode() {
      return (msgId * 1000 + srcId);
    }
    public void copyTo(DataItem data) {
      data.srcId = srcId;
      data.dstId = dstId;
      data.msgId = msgId;
      data.addTime = addTime;
      data.timeout = timeout;
      data.tickets = tickets;
      data.hops = hops;
    }
    public DataItem getClone() {
      DataItem data = new DataItem();
      copyTo(data);
      return data;
    }
  } // end class TableItem

  /** class ContactInfo */
  protected static class ContactInfo implements DoNotRewrite
  {
    public int id = -1;
  } // end class ContactInfo

  /** class UpdateMessage */
  public static class DataMessage implements Message, DoNotRewrite
  {
    public long time;
    public DataItem data;
    public int getSize() {
      return DATA_SIZE;
    }
    public void getBytes(byte[] msg, int offset) {
      throw new RuntimeException("no implementation.");
    }
    public HashMap tickets;
  } // end class UpdateMessage

  /** class Cleaner */
  public class Cleaner implements Timer, DoNotRewrite
  {
    public void start() {
      Clock.start(this, CLEANER_INTERVAL, false);
    }
    public void timeout() {
      cleanBuffer();
      start();
    }
  } // end class Cleaner

  /** class Cleaner */
  public class CBR implements Timer, DoNotRewrite
  {
    protected int msgId = 0;
    public void start() {
      long delay = (long)(DATA_INTERVAL * 2 * Constants.random.nextDouble());
      Clock.start(this, delay, false);
    }
    public void timeout() {
      ++ msgId;
      DataItem data1 = new DataItem();
      data1.srcId = node.getId();
      data1.dstId = 1 + Constants.random.nextInt(Field.NODES);
      data1.msgId = msgId;
      data1.timeout = Clock.getTime() + DATA_TTL;
      data1.tickets = DATA_TICKETS;
      addData(data1);
      ++ Stats.data;
      start();
    }
  } // end class Cleaner

  ///////////////////////////
  // variables
  //

  public DataItem[] datas = new DataItem[BUFFER_SIZE];
  protected Node node;
  protected HashMap contacts = new HashMap();
  protected HashMap index = new HashMap();
  public long updateTime = INIT_TIME;
  protected Cleaner cleaner = new Cleaner();
  protected CBR cbr = new CBR();

  public Data(Node node) {
    this.node = node;
    cleaner.start();
    /* CBR */
    cbr.start();
  }

  public void onContactDetected(int contactId) {
    node.startJitterTimer();
    ContactInfo contact = new ContactInfo();
    contact.id = contactId;
    contacts.put(new Integer(contact.id), contact);
  }

  public void onContactBroken(int contactId) {
    contacts.remove(new Integer(contactId));
  }

  public void receiveMessage(Message message, int srcId) {
    DataMessage msg = (DataMessage)message;
    //System.out.println(node.getId() + " received " + msg.data);
    DataItem data = msg.data.getClone();
    long curTime = Clock.getTime();
    data.addTime = curTime;
    data.timeout = msg.data.timeout + (curTime - msg.time);
    data.tickets = 0;
    Integer tickets = (Integer)msg.tickets.get(new Integer(node.getId()));
    if (tickets != null) {
      data.tickets = tickets.intValue();
    }
    if (data.tickets != 0 && data.dstId == node.getId()) {
      if (node.ack.addAck(data.srcId, data.msgId, data.timeout)) {
        node.timeStats.incRecvDataMsg(msg.getSize(), msg.data.tickets, msg.data.hops);
        ++ Stats.acks;
        Stats.delay += curTime - (msg.data.timeout - DATA_TTL);
      }
    }
    else {
      if (data.tickets != 0) {
        addData(data);
        updateTime = curTime;
      }
      //addData(data);
      //if (data.tickets != 0) {
      //  updateTime = curTime;
      //}
    }
  }

  public Message nextMessageToSend() {
    LinkedList contacts = new LinkedList();
    Iterator it = this.contacts.keySet().iterator();
    while (it.hasNext()) {
      Integer contact = (Integer)it.next();
      int contactId = contact.intValue();
      if (! node.ack.updateReceived(contactId)) continue;
      if (! node.vector.updateReceived(contactId)) continue;
      contacts.addLast(contact);
    }
    if (contacts.size() == 0) return null;
    DataMessage msg = null;
    double p1 = DTDV_ALLOCATION;
    if (p1 < -0.001) {
      p1 = node.timeStats.newDtdvAllocationPoint();
    }
    double p2 = node.timeStats.currentDtdvAllocationPoint();
    if (p2 < p1) {
      msg = nextMessageToSend_DTDV(contacts);
      if (msg != null) {
        node.timeStats.incDtdvSendDataMsg(msg.getSize());
      }
      if (msg == null) {
        msg = nextMessageToSend_SW(contacts);
        if (msg != null) {
          node.timeStats.incSwSendDataMsg(msg.getSize());
        }
      }
    }
    else {
      msg = nextMessageToSend_SW(contacts);
      if (msg != null) {
        node.timeStats.incSwSendDataMsg(msg.getSize());
      }
      if (msg == null) {
        msg = nextMessageToSend_DTDV(contacts);
        if (msg != null) {
          node.timeStats.incDtdvSendDataMsg(msg.getSize());
        }
      }
    }
    if (msg != null) {
      it = msg.tickets.keySet().iterator();
      while (it.hasNext()) {
        Integer id = (Integer)it.next();
        Integer tickets = (Integer)msg.tickets.get(id);
        if (tickets.intValue() == 0) continue;
        node.vector.addVector(id.intValue(), msg.data.srcId, msg.data.msgId, msg.data.timeout);
      }
    }
    return msg;
  }

  public DataMessage nextMessageToSend_SW(LinkedList contacts) {
    if (! SW_ENABLED) return null;
    DataItem best = null;
    int bestScore = 0;
    for (int i = 0; i < datas.length; ++ i) {
      if (datas[i] == null || datas[i].tickets == 0) continue;
      int score = 0;
      Iterator it = contacts.iterator();
      while (it.hasNext()) {
        Integer contact = (Integer)it.next();
        if (! node.vector.containsVector(contact.intValue(), datas[i].srcId, datas[i].msgId)) {
          ++ score;
        }
        if (contact.intValue() == datas[i].dstId) {
          score += 100;
        }
      }
      score *= datas[i].tickets;
      if (bestScore < score) {
        best = datas[i];
        bestScore = score;
      }
    }
    if (best == null) return null;
    DataMessage msg = new DataMessage();
    msg.time = Clock.getTime();
    msg.data = best.getClone();
    msg.tickets = new HashMap();
    if (contacts.contains(new Integer(best.dstId))) {
      msg.tickets.put(new Integer(best.dstId), new Integer(best.tickets));
      best.tickets = 0;
    }
    else {
      if (best.tickets == 1) return null;
      Iterator it = contacts.iterator();
      while (it.hasNext()) {
        Integer contact = (Integer)it.next();
        if (node.vector.containsVector(contact.intValue(), best.srcId, best.msgId)) {
          it.remove();
        }
      }
      int[] tickets = new int[contacts.size()];
      int avg = best.tickets / (tickets.length + 1);
      int res = best.tickets % (tickets.length + 1);
      best.tickets = avg;
      if (res > 0) {
        ++ best.tickets;
        -- res;
      }
      for (int i = 0; i < tickets.length; ++ i) {
        tickets[i] = avg;
        if (res > 0) {
          ++ tickets[i];
          --res;
        }
      }
      for (int i = 0; i < tickets.length; ++ i) {
        msg.tickets.put(contacts.removeFirst(), new Integer(tickets[i]));
      }
    }
    msg.data.tickets -= best.tickets;
    ++ Stats.send.dataMsgs2;
    Stats.send.dataMsgSize2 += msg.getSize();
    //System.out.println(node.getId() + " send: " + msg.data);
    return msg;
  }

  public DataMessage nextMessageToSend_DTDV(LinkedList contacts) {
    if (! DTDV_ENABLED) return null;
    long curTime = Clock.getTime();
    DataItem best = null;
    int hops = Integer.MAX_VALUE;
    int nextHop = -1;
    for (int i = 0; i < datas.length; ++ i) {
      if (datas[i] == null) continue;
      if (datas[i].timeout <= curTime) {
        index.remove(datas[i]);
        datas[i] = null;
        continue;
      }
      if (datas[i].tickets == 0) continue;
      DTDV.TableItem item = (DTDV.TableItem)node.dtdv.table.get(new Integer(datas[i].dstId));
      if (item == null || item.hop == Integer.MAX_VALUE) continue;
      if (! contacts.contains(new Integer(item.next))) continue;
      if (node.vector.containsVector(item.next, datas[i].srcId, datas[i].msgId)) continue;
      if (hops > item.hop) {
        best = datas[i];
        hops = item.hop;
        nextHop = item.next;
      }
      else if (hops == item.hop) {
        if (datas[i].tickets > best.tickets) {
          best = datas[i];
          nextHop = item.next;
        }
      }
    }
    if (best == null) return null;
    DataMessage msg = new DataMessage();
    msg.time = Clock.getTime();
    msg.data = best.getClone();
    ++ msg.data.hops;
    msg.tickets = new HashMap();
    msg.tickets.put(new Integer(nextHop), new Integer(best.tickets));
    best.tickets = 0;
    ++ Stats.send.dataMsgs1;
    Stats.send.dataMsgSize1 += msg.getSize();
    //System.out.println("[" + msg.data.hops + "]");
    return msg;
  }

  public void onAckMessage(Message message) {
    DataMessage msg = (DataMessage)message;
    if (msg.data.hops > 0) {
      ++ Stats.recv.dataMsgs1;
      Stats.recv.dataMsgSize1 += msg.getSize();
    }
    else {
      ++ Stats.recv.dataMsgs2;
      Stats.recv.dataMsgSize2 += msg.getSize();
    }
  }

  /** rollback the update information */
  public void onDropMessage(Message message, int[] dstIds) {
    DataMessage msg = (DataMessage)message;
    boolean dropped = false;
    for (int i = 0; i < dstIds.length; ++ i) {
      int unacked = dstIds[i];
      Integer tickets = (Integer)msg.tickets.get(new Integer(unacked));
      if (tickets == null) continue;
      dropped = true;
      Integer ind = (Integer)index.get(msg.data);
      if (ind != null) {
        DataItem data = datas[ind.intValue()];
        data.tickets += tickets.intValue();
      }
      if (tickets.intValue() > 0) {
        node.vector.removeVector(unacked, msg.data.srcId, msg.data.msgId);
      }
    }
    if (! dropped) {
      onAckMessage(message);
    }
  }

  public boolean addData(DataItem data) {
    if (node.ack.containsAck(data.srcId, data.msgId)) {
      return false;
    }
    Integer ind = (Integer)index.get(data);
    if (ind != null) {
      datas[ind.intValue()].tickets += data.tickets;
    }
    else {
      long curTime = Clock.getTime();
      int empty = -1;
      int minTickets = data.tickets;
      int minIndex = -1;
      long minTimeout = data.timeout;
      for (int i = 0; i < datas.length; ++ i) {
        if (datas[i] == null) {
          if (empty == -1) empty = i;
        }
        else if (datas[i].timeout <= curTime) {
          index.remove(datas[i]);
          datas[i] = null;
          if (empty == -1) empty = i;
        }
        else {
          if (minTickets > datas[i].tickets) {
            minTickets = datas[i].tickets;
            minIndex = i;
          }
          else if (minTickets == datas[i].tickets) {
            if (minTimeout > datas[i].timeout) {
              minTimeout = datas[i].timeout;
              minIndex = i;
            }
          }
        }
      }
      if (empty == -1) {
        if (minIndex == -1) {
          return false;
        }
        index.remove(datas[minIndex]);
        datas[minIndex] = null;
        empty = minIndex;
      }
      index.put(data, new Integer(empty));
      datas[empty] = data;
      data.addTime = curTime;
    }
    return true;
  }

  public boolean removeData(int srcId, int msgId) {
    DataItem data = new DataItem();
    data.srcId = srcId;
    data.msgId = msgId;
    Integer ind = (Integer)index.remove(data);
    if (ind == null) return false;
    datas[ind.intValue()] = null;
    return true;
  }

  public int cleanBuffer() {
    int size = 0;
    long curTime = Clock.getTime();
    for (int i = 0; i < datas.length; ++ i) {
      if (datas[i] == null) continue;
      if (datas[i].timeout <= curTime) {
        index.remove(datas[i]);
        datas[i] = null;
      }
      else {
        ++ size;
      }
    }
    return size;
  }

  public String toString() {
    String buf = "";
    for (int i = 0; i < datas.length; ++ i) {
      if (datas[i] == null) continue;
      buf += datas[i] + "\r\n";
    }
    return buf;
  }

}
